package roborally.model;

import java.util.*;
import roborally.model.interfaces.*;

import be.kuleuven.cs.som.annotate.*;

/**
 *@Invar	|this.getBoard().isValidPosition(this.getPosition())
 *@Invar	|hasProperElements()
 *@Implements	Terminatable	
 */
public class Field implements Terminatable{
	
	//CONSTRUCTOR
	public Field(Board board, Position position) throws IllegalArgumentException //a field cannot be created without a given board and position
	{
		if(board == null) throw new IllegalArgumentException("Ineffective board given while instantiating this field object.");
		this.myBoard = board;
		if(!board.isValidPosition(position)) throw new IllegalArgumentException("Position is out of the bounds of the associated board.");
		this.position = position;
	}
	
	//TERMINATE	
	/**
	 * return 	|result == isTerminated
	 */
	@Basic @Raw
	public boolean isTerminated(){ return isTerminated; }
	/**
	 * Terminates a Field object.
	 * @effect	|for (Element element : getElements().toArray(new Element[0]))
	 *			|	element.terminate()
	 * @post 	|new.isTerminated == true
	 * @post 	|new.getElements().isEmpty() == true
	 * @post	|new.getBoard().getFieldsInUse().contains(this) == false	
	 */
	public void terminate(){
		if(isTerminated) return;
		//when a field is terminated, all the elements placed on this field will be terminated as well.
		for (Element element : getElements().toArray(new Element[0])) //we must first create an array of element objects from our set, otherwise there will be a ConcurrentModificationException.
			element.terminate();
		//remove the reference to this field from the board.
		//remember! When calling terminate() on an object, the object is responsible for removing all references to itself.
		getBoard().removeField(this);
		//now terminate this field by setting isTerminated to true.
		this.isTerminated = true;
	}
	private boolean isTerminated;
	
	//INSPECTORS
	/**
	 * @return	|result == (getRobot() != null)
	 */
	@Raw
	public boolean hasRobot(){ return getRobot() != null; }
	/**
	 * @return	|result == (getWall() != null)
	 */
	@Raw
	public boolean hasWall(){ return getWall() != null; }
	/**
	 * @return	|result == (!getItems().isEmpty())
	 */
	@Raw
	public boolean hasItem(){ return !getItems().isEmpty(); }
	/**
	 * @return	|result == (!getBatteries().isEmpty())
	 */
	@Raw
	public boolean hasBattery(){ return !getBatteries().isEmpty(); }
	
	//VALIDATORS
	/**
	 * 
	 * @return 	|let
	 * 			|	boolean valid = true
	 * 			|	for( Element element : getElements() )
	 *			|		for( Element otherElement : getElements() )
	 *			|			if(!presentElement.canHaveSameFieldAs(element))
	 *			|				then valid = false;
	 *			|in
	 *			|	result == valid
	 */
	@Raw
	public boolean hasProperElements(){
		for( Element element : getElements() )
			for( Element otherElement : getElements() )
				if(!element.canHaveSameFieldAs(otherElement)) return false; //as soon as one of the elements on this field can not be placed on the same field as another element, return false
		return true;
	}
	/**
	 * Checks whether the given element can be put on this field
	 * @param element
	 * @return 	|let
	 * 			|	boolean valid = true
	 * 			|	for( Element presentElement : getElements() )
	 *			|		if(!presentElement.canHaveSameFieldAs(element))
	 *			|			then valid = false;
	 *			|in
	 *			|	result == valid
	 */
	@Raw
	public boolean canPutElement(Element element){
		for( Element presentElement : getElements() ) {
			if(!presentElement.canHaveSameFieldAs(element)) //as soon as one of the elements on this field can not be placed on the same field as 'element', return false
				return false;
		}
		return true;
	}

	//Associations and attributes
	/**
	 * @return Returns the position of the field as an instance of Position
	 */
	@Basic @Raw
	public Position getPosition(){ return this.position; }
	
	/**
	 * 	A Position instance that contains two longs, respectively for x- and y-coordinate.
	 * 	The x-coordinate is then accessed by: position.getX()
	 * 	and for the y-coordinate: position.getY()
	 */
	private final Position position;
	
	@Basic @Immutable @Raw
	public Board getBoard(){ return myBoard; }
	
	/**
	 * a reference to a board object, representing a final association with that board.
	 */
	private final Board myBoard; 
	
	/**
	 * contains all of the elements that are placed on this field.
	 */
	private Set<Element> myElements = new HashSet<Element>();
	
	/**
	 * @return |result == this.myElements
	 */
	@Basic @Raw
	public Set<Element> getElements(){ return myElements; }
	
	/**
	 * @return |result == !getElements().isEmpty()
	 */
	public boolean hasElement(){ return !getElements().isEmpty(); }
	
	/**
	 * method that returns the robot on this field if it is present, else null is returned
	 * @return 	|let
	 * 			|	for (Element element : myElements)
	 * 			|in
	 * 			|	if(element instanceof Robot) result == (Robot)element
	 */
	@Raw
	public Robot getRobot(){ 
		for (Element element : myElements) {
			if( element instanceof Robot ) 
				return (Robot)element;
		}
		return null;
	}
	
	/**
	 * method that returns the wall on this field if it is present, else null is returned
	 * @return 	|let
	 * 			|	for (Element element : myElements)
	 * 			|in
	 * 			|	if(element instanceof Wall) result == (Wall)element
	 */
	@Raw
	public Wall getWall(){ 
		for (Element element : myElements) {
			if( element instanceof Wall ) 
				return (Wall)element;
		}
		return null;
	}

	/**
	 * A method that returns all the items that are placed on this Field
	 * @return 	|let
	 * 			| 	Set<Item> items = new HashSet<Item>()
	 *			|	for (Element element : myElements) {
	 *			|		if( element instanceof Item ) 
	 *			|			items.add((Item)element)
	 *			|in
	 *			|	result == items
	 */
	@Raw
	public Set<Item> getItems(){
		Set<Item> items = new HashSet<Item>();
		for (Element element : myElements) {
			if( element instanceof Item ) 
				items.add((Item)element);
		}
		return items;
	}
	
	/**
	 * A method that returns all the batteries that are placed on this Field
	 * @return 	|let
	 * 			| 	Set<Battery> batteries = new HashSet<Battery>()
	 *			|	for (Element element : myElements) {
	 *			|		if( element instanceof Battery ) 
	 *			|			batteries.add((Battery)element)
	 *			|in
	 *			|	result == batteries
	 */
	@Raw
	public Set<Battery> getBatteries(){
		Set<Battery> batteries = new HashSet<Battery>();
		for (Element element : myElements) {
			if( element instanceof Battery ) 
				batteries.add((Battery)element);
		}
		return batteries;
	}
	
	/**
	 * A method that returns all the repairKits that are placed on this Field
	 * @return 	|let
	 * 			| 	Set<RepairKit> repairKits = new HashSet<RepairKit>()
	 *			|	for (Element element : myElements) {
	 *			|		if( element instanceof RepairKit ) 
	 *			|			repairKits.add((RepairKit)element)
	 *			|in
	 *			|	result == repairKits
	 */
	@Raw
	public Set<RepairKit> getRepairKits(){
		Set<RepairKit> repairKits = new HashSet<RepairKit>();
		for (Element element : myElements) {
			if( element instanceof RepairKit ) 
				repairKits.add((RepairKit)element);
		}
		return repairKits;
	}
	
	/**
	 * A method that returns all the surpriseBoxes that are placed on this Field
	 * @return 	|let
	 * 			| 	Set<SurpriseBox> surpriseBoxes = new HashSet<SurpriseBox>()
	 *			|	for (Element element : myElements) {
	 *			|		if( element instanceof SurpriseBox ) 
	 *			|			surpriseBoxes.add((SurpriseBox)element)
	 *			|in
	 *			|	result == surpriseBoxes
	 */
	@Raw
	public Set<SurpriseBox> getSurpriseBoxes(){
		Set<SurpriseBox> surpriseBoxes = new HashSet<SurpriseBox>();
		for (Element element : myElements) {
			if( element instanceof SurpriseBox ) 
				surpriseBoxes.add((SurpriseBox)element);
		}
		return surpriseBoxes;
	}
	
	/** 
	 * A method that returns one random item of the set of items located on this Field.
	 * 
	 * @return 	|let
	 * 			| 	int rand = (new Random()).nextInt(getItems().size())
	 *			|in
	 *			|	result == (Item)this.getItems().toArray()[rand];
	 */
	@Raw
	public Item getRandomItem(){
		int rand = (new Random()).nextInt(getItems().size());
		return (Item)this.getItems().toArray()[rand];
	}
	
	//****************************** put and remove, manage bidirectional associations ************************************************
	//Put method (special substitute for set-method), we set up a bidirectional association!
	/**
	 * Associates an element with this field.
	 * @param element
	 * @effect	|element.placeOnBoard(this)
	 * @effect	|getElements().add(element)
	 * @throws IllegalArgumentException
	 * 			|element == null
	 * @throws IllegalStateException
	 * 			|!canPutElement(element)
	 */
	public void putElement(Element element) throws IllegalArgumentException, IllegalStateException
	{
		if(element == null) throw new IllegalArgumentException("Argument 'element' == null");	
		if(!canPutElement(element)) throw new IllegalStateException("Element could not be placed on this field because of other present element(s).");
		element.placeOnBoard(this); //we associate the field with this element
		getElements().add(element); //and associate the element with the field
	}
	
	//Remove method, we end the bidirectional association!
	/**
	 * Ends the association between the given element and this field. (The element is removed from this field.)
	 * @param element
	 * @effect |getElements().remove(element)
	 * @effect |if(getElements().remove(element)) then element.removeFromBoard()
	 * @return |if(getElements().remove(element)) then result == element.removeFromBoard()
	 * 			|else result == false
	 * @throws IllegalArgumentException
	 */
	public boolean removeElement(Element element) throws IllegalArgumentException
	{
		if(element == null) throw new IllegalArgumentException("Argument 'item' == null");
		if(getElements().remove(element)) //if the element is present on this field, it will be removed and true is returned (we go into the if-structure)
			return element.removeFromBoard();//We also remove its reference to its field.
		return false;
	}
	
	//Other mutators
	/**
	 * @param otherField
	 * @effect	|	for (Element element : otherField.getElements()){
	 *			|try
	 *			|	otherField.removeElement(element)
	 *			|	this.putElement(element)
	 *			|catch (IllegalArgumentException argExc){
	 *			|	otherField.putElement(element)
	 *			|catch (IllegalStateException stateExc
	 *			|	otherField.putElement(element)
	 */
	@Raw
	public void merge(@Raw Field otherField) {
		//try to move the (possibly present) elements to this field
		for (Element element : otherField.getElements()){
			try{
				otherField.removeElement(element);
				this.putElement(element);	
			}
			catch (IllegalArgumentException argExc){
				otherField.putElement(element); } //we couldnt move this element, so we put it back on otherField, to be terminated together with otherField after the merging.
			catch (IllegalStateException stateExc){
				otherField.putElement(element); }
		}
	}
	
	/**
	 * a method that returns whether or not the given field is in the given direction respective to this field.
	 * @param otherField
	 * @param orientation
	 * @return	|let
	 * 			|	Position diff = getPosition().getDifferenceVector(otherField.getPosition())
	 *			|	Orientation diffOr = Orientation.positionVectorToOrientation(diff)
	 *			|in
	 *			|	result == (diffOr != null) && diffOr.equals(orientation)
	 */
	public boolean isOtherInDirection(Field otherField, Orientation orientation){
		Position diff = getPosition().getDifferenceVector(otherField.getPosition());
		Orientation diffOr = Orientation.positionVectorToOrientation(diff);
		return (diffOr != null) && diffOr.equals(orientation); //and if the field is in the desired direction
	}
	
	/**
	 * a method that returns the field closest to this field out of a given list of fields.
	 * @param fields
	 * @return |if(fields == null || fields.isEmpty()) then result == null;
	 * @return |let
	 * 			|	List<Position> positions = new ArrayList<Position>()
	 *			|	for(Field field : fields)
	 *			|		positions.add(field.getPosition())
	 *			|	Position nearestPos = this.getPosition().getNearestPosition(positions)
	 *			|	Field resultField = null
	 *			|	for(Field field : fields)
	 *			|		if(field.getPosition().equals(nearestPos)) resultfield = field
	 *			|in
	 *			|	result == resultField
	 */
	public Field getNearestField(List<Field> fields)
	{
		if(fields == null || fields.isEmpty()) return null;
		List<Position> positions = new ArrayList<Position>();
		for(Field field : fields)
			positions.add(field.getPosition());
		Position nearestPos = this.getPosition().getNearestPosition(positions);
		for(Field field : fields)
			if(field.getPosition().equals(nearestPos)) return field;
		return null;
	}
	
	/**
	 * @return	A string indicating the position of the field and the nature of the elements placed on it
	 */
	@Override
	public String toString(){
		String s = "Position: "+getPosition().toString()+"|";
		int i = 0;
		for (Element element : getElements())
			s += ", element_" + i + " " + element.getClass();
		return s;
	}
}
